# python3
# Copyright 2021 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import toml
import unittest

from lib import gen
from lib import cargo
from lib import compiler

# An example output from `cargo tree`
# 1. We patch paths into the Cargo.toml files that we feed to `cargo tree` and
#    this demonstrates that for the "quote" crate by including the path there.
# 2. We include features in the output, demonstrated by many deps here. And we
#    also have an example of a crate without any features with the second
#    occurrence of "unicode-xid".
# 3. Some crates get de-duped and that means get a (*) on them, which we can
#    see.
# 4. A proc-macro crate is a different kind of crate and is designated by the
#    (proc-macro) after the version, and we have an example of that with
#    cxxbridge-macro.
# 5. We include direct deps as well as transitive deps, which have more than one
#    piece of ascii art on their line.
CARGO_TREE = """
cxx v1.0.56 (/path/to/chromium/src/third_party/rust/cxx/v1/crate)
├── cxxbridge-macro v1.0.56 (proc-macro)
│   ├── proc-macro2 v1.0.32 default,proc-macro,span-locations
│   │   └── unicode-xid v0.2.2 default
│   ├── quote v1.0.10 (/path/to/chromium/src/third_party/rust/quote/v1/crate) default,proc-macro
│   │   └── proc-macro2 v1.0.32 default,proc-macro,span-locations (*)
│   └── syn v1.0.81 clone-impls,default,derive,full,parsing,printing,proc-macro,quote
│       ├── proc-macro2 v1.0.32 default,proc-macro,span-locations (*)
│       ├── quote v1.0.10 default,proc-macro (*)
│       └── unicode-xid v0.2.2
└── link-cplusplus v1.0.5 default
""".lstrip("\n")


class GenTestCase(unittest.TestCase):
    def assertListSortedEqual(self, a, b, msg=None):
        a.sort()
        b.sort()
        if msg:
            self.assertListEqual(a, b, msg=msg)
        else:
            self.assertListEqual(a, b)

    def matching_archs(self, matching: str) -> set[str]:
        return {
            arch
            for arch in compiler._RUSTC_ARCH_TO_BUILD_CONDITION
            if re.search(matching, arch)
        }

    def all_archs(self):
        return set(compiler._RUSTC_ARCH_TO_BUILD_CONDITION.keys())

    def test_parse_cargo_tree_dependency_line(self):
        lines = CARGO_TREE.split("\n")

        # Here we are simulating `cargo tree` on a third-party Cargo.toml file,
        # not our special third_party.toml file. So we pass False as
        # is_third_party_toml, and we can give an empty parsed toml file as it
        # won't be used then.

        #cxx v1.0.56 (/path/to/chromium/src/third_party/rust/cxx/v1/crate)
        r = gen._parse_cargo_tree_dependency_line(dict(), False, lines[0])
        self.assertEqual(r, None)  # not a dependency

        #├── cxxbridge-macro v1.0.56 (proc-macro)
        r = gen._parse_cargo_tree_dependency_line(dict(), False, lines[1])
        expected = gen.CargoTreeDependency(cargo.CrateKey(
            "cxxbridge-macro", "1.0.56"),
                                           is_proc_macro=True)
        self.assertEqual(r, expected)

        #│   ├── proc-macro2 v1.0.32 default,proc-macro,span-locations
        r = gen._parse_cargo_tree_dependency_line(dict(), False, lines[2])
        expected = gen.CargoTreeDependency(
            cargo.CrateKey("proc-macro2", "1.0.32"),
            features=["default", "proc-macro", "span-locations"])
        self.assertEqual(r, expected)

        #│   │   └── unicode-xid v0.2.2 default
        r = gen._parse_cargo_tree_dependency_line(dict(), False, lines[3])
        expected = gen.CargoTreeDependency(cargo.CrateKey(
            "unicode-xid", "0.2.2"),
                                           features=["default"])
        self.assertEqual(r, expected)

        #│   ├── quote v1.0.10 (/path/to/chromium/src/third_party/rust/quote/v1/crate) default,proc-macro
        r = gen._parse_cargo_tree_dependency_line(dict(), False, lines[4])
        expected = gen.CargoTreeDependency(cargo.CrateKey("quote", "1.0.10"),
                                           features=["default", "proc-macro"])
        self.assertEqual(r, expected)

        #│   │   └── proc-macro2 v1.0.32 default,proc-macro,span-locations (*)
        r = gen._parse_cargo_tree_dependency_line(dict(), False, lines[5])
        expected = gen.CargoTreeDependency(
            cargo.CrateKey("proc-macro2", "1.0.32"),
            features=["default", "proc-macro", "span-locations"])
        self.assertEqual(r, expected)

        #│   └── syn v1.0.81 clone-impls,default,derive,full,parsing,printing,proc-macro,quote
        r = gen._parse_cargo_tree_dependency_line(dict(), False, lines[6])
        expected = gen.CargoTreeDependency(cargo.CrateKey("syn", "1.0.81"),
                                           features=[
                                               "clone-impls", "default",
                                               "derive", "full", "parsing",
                                               "printing", "proc-macro", "quote"
                                           ])
        self.assertEqual(r, expected)

        #│       ├── proc-macro2 v1.0.32 default,proc-macro,span-locations (*)
        r = gen._parse_cargo_tree_dependency_line(dict(), False, lines[7])
        expected = gen.CargoTreeDependency(
            cargo.CrateKey("proc-macro2", "1.0.32"),
            features=["default", "proc-macro", "span-locations"])
        self.assertEqual(r, expected)

        #│       ├── quote v1.0.10 default,proc-macro (*)
        r = gen._parse_cargo_tree_dependency_line(dict(), False, lines[8])
        expected = gen.CargoTreeDependency(cargo.CrateKey("quote", "1.0.10"),
                                           features=["default", "proc-macro"])
        self.assertEqual(r, expected)

        #│       └── unicode-xid v0.2.2
        r = gen._parse_cargo_tree_dependency_line(dict(), False, lines[9])
        expected = gen.CargoTreeDependency(
            cargo.CrateKey("unicode-xid", "0.2.2"))
        self.assertEqual(r, expected)

        #└── link-cplusplus v1.0.5 default
        r = gen._parse_cargo_tree_dependency_line(dict(), False, lines[10])
        d = gen.CargoTreeDependency(cargo.CrateKey("link-cplusplus", "1.0.5"),
                                    features=["default"])
        self.assertEqual(r, d)

    def test_third_party_toml(self):
        THIRD_PARTY_TOML = """
# This dependency has no extensions
[dependencies]
cxxbridge-macro = "1"

# This dependency has an extension, and is default-visible to first-party code.
[dependencies.link-cplusplus]
build-script-outputs = [ "src/link.rs", "src/cplusplus.rs" ]

# This dependency is not visible to first-party code thanks to the extension.
[dependencies.syn]
allow-first-party-usage = false
"""

        # Here we are simulating parsing our special third_party.toml file, so
        # we need to present the contents of that file to the
        # _parse_cargo_tree_dependency_line() function, in order for it to look
        # for extensions.
        toml_content = toml.loads(THIRD_PARTY_TOML)

        line = "├── cxxbridge-macro v1.0.56 (proc-macro)"
        r = gen._parse_cargo_tree_dependency_line(toml_content, True, line)
        d = gen.CargoTreeDependency(
            cargo.CrateKey("cxxbridge-macro", "1.0.56"),
            is_proc_macro=True,
            # Deps from third_party.toml are visible to first-party code by
            # default.
            is_for_first_party_code=True)
        self.assertEqual(r, d)

        line = "├── link-cplusplus v1.0.5 default"
        r = gen._parse_cargo_tree_dependency_line(toml_content, True, line)
        d = gen.CargoTreeDependency(
            cargo.CrateKey("link-cplusplus", "1.0.5"),
            features=["default"],
            is_for_first_party_code=True,
            # link-cplusplus has build script outputs listed in our
            # third_party.toml.
            build_script_outputs={"src/link.rs", "src/cplusplus.rs"})
        self.assertEqual(r, d)

        line = "└── syn v1.0.81 clone-impls,default"
        r = gen._parse_cargo_tree_dependency_line(toml_content, True, line)
        d = gen.CargoTreeDependency(
            cargo.CrateKey("syn", "1.0.81"),
            features=["clone-impls", "default"],
            # Deps from third_party.toml are visible to first-party code unless
            # they opt out explicitly, which our syn dependency has done.
            is_for_first_party_code=False)
        self.assertEqual(r, d)

    def test_get_archs_third_party_toml(self):
        crate_key = None  # For third_party.toml there's none.
        usage_data = None  # For third_party.toml there's none.

        # Test what happens when there is a `target` on a dependency.
        toml_content = toml.loads("""
[dependencies]
all-platform-crate = "1"

[target."cfg(windows)".dependencies]
windows-only-crate = "2"
        """)

        # Test case without a target specified on the command line. Since
        # there's differences per-architecture in the TOML, we will have to test
        # all architectures.
        arch_specific = gen._get_archs_of_interest(toml_content, usage_data,
                                                   None)
        # Not true as there's a arch-specific dependency.
        self.assertFalse(arch_specific.is_single_arch())
        self.assertSetEqual(self.all_archs(), arch_specific.archs_to_test())

        # Test case with a target specified on the command line that matches
        # the target. We only need to test the target the user specified.
        arch_specific = gen._get_archs_of_interest(toml_content, usage_data,
                                                   "x86_64-pc-windows-msvc")
        # We only ever care about one arch, so everything is single-arch.
        self.assertTrue(arch_specific.is_single_arch())
        self.assertSetEqual({"x86_64-pc-windows-msvc"},
                            arch_specific.archs_to_test())

        # Test case with a target specified on the command line that does not
        # match the target. We only need to test the target the user specified,
        # and we're not smart enough to prune it yet..
        arch_specific = gen._get_archs_of_interest(toml_content, usage_data,
                                                   "x86_64-unknown-linux-gnu")
        # We only ever care about one arch, so everything is single-arch.
        self.assertTrue(arch_specific.is_single_arch())
        self.assertSetEqual({"x86_64-unknown-linux-gnu"},
                            arch_specific.archs_to_test())

        # Test what happens when there is no `target` on a dependency.
        toml_content = toml.loads("""
[dependencies]
all-platform-crate = "1"
        """)

        # Test case without a target specified on the command line.
        arch_specific = gen._get_archs_of_interest(toml_content, usage_data,
                                                   None)
        # Is true since we can apply a single architecture's result to other
        # ones.
        self.assertTrue(arch_specific.is_single_arch())
        # We only need to test one architecture and copy its results to the
        # rest.
        self.assertEqual(len(arch_specific.archs_to_test()), 1)

        # We don't care which architecture will be tested, but in the next test
        # case we do, so make sure the arbitrary architecture here isn't doesn't
        # unluckily collide with our next test.
        self.assertFalse({"x86_64-pc-windows-msvc"
                          }.issubset(arch_specific.archs_to_test()))

        # Test case with a target specified on the command line.
        arch_specific = gen._get_archs_of_interest(toml_content, usage_data,
                                                   "x86_64-pc-windows-msvc")
        self.assertTrue(arch_specific.is_single_arch())
        # We should test the architecture specified.
        self.assertSetEqual({"x86_64-pc-windows-msvc"},
                            arch_specific.archs_to_test())

    def make_fake_parent(self, child_name: str,
                         archset_where_parent_used: compiler.ArchSet,
                         parent_is_arch_specific: bool,
                         parent_has_arch_specific_deps: bool
                         ) -> gen.ArchSpecific:
        if not parent_has_arch_specific_deps:
            toml_content = toml.loads("""
[dependencies]
{} = "1"
          """.format(child_name))
        else:
            toml_content = toml.loads("""
[target."cfg(windows)".dependencies]
{} = "1"
        """.format(child_name))
        parent_crate_data = gen.PerCrateData()
        parent_crate_data.arch_specific = parent_is_arch_specific
        parent_crate_data.used_on_archs = archset_where_parent_used
        return gen._get_archs_of_interest(toml_content, parent_crate_data, None)

    def make_fake_dep(self, dep_key: cargo.CrateKey,
                      parent_requested_features_of_dep: list[str],
                      dep_is_proc_macro: bool, dep_for_first_party_code: bool,
                      dep_build_script_outputs: set[str]):
        return gen.CargoTreeDependency(
            dep_key,
            features=parent_requested_features_of_dep,
            is_proc_macro=dep_is_proc_macro,
            is_for_first_party_code=dep_for_first_party_code,
            build_script_outputs=dep_build_script_outputs)

    def add_fake_dependency_edges(self, build_data: gen.BuildData,
                                  new_keys: set[cargo.CrateKey], **kwargs):
        parent_name: str = kwargs["parent_name"]
        parent_arch_specific = self.make_fake_parent(
            kwargs["dep_name"], kwargs["archset_where_parent_used"],
            kwargs["parent_is_arch_specific"],
            kwargs["parent_has_arch_specific_deps"])
        dep_key = cargo.CrateKey(kwargs["dep_name"], "1.2.3")
        dep = self.make_fake_dep(dep_key,
                                 kwargs["parent_requested_features_of_dep"],
                                 kwargs["dep_is_proc_macro"],
                                 kwargs["dep_for_first_party_code"],
                                 kwargs["dep_build_script_outputs"])

        parent_crate_key = cargo.CrateKey(parent_name,
                                          "1.2.3") if parent_name else None

        gen._add_edges_for_dep_on_target_arch(build_data, parent_crate_key,
                                              parent_arch_specific,
                                              kwargs["parent_usage"],
                                              kwargs["parent_output"], dep,
                                              kwargs["computing_arch"],
                                              new_keys)
        return dep_key

    def test_add_edges(self):
        build_data = gen.BuildData()
        new_keys = set()

        for_first_party_code = True
        build_script_outputs = {"src/child-outs.rs"}

        dep_key = self.add_fake_dependency_edges(
            build_data,
            new_keys,
            parent_name=None,  # No parent crate from third_party.toml.
            # Parent used everywhere.
            archset_where_parent_used=compiler.ArchSet.ALL(),
            parent_is_arch_specific=False,
            parent_has_arch_specific_deps=False,
            # computing_arch is chosen arbitrarily since the result will apply
            # to all as parent_has_arch_specific_deps is False
            computing_arch="aarch64-apple-darwin",
            # Parent used for a binary.
            parent_usage=cargo.CrateUsage.FOR_NORMAL,
            # Adding a build-dependency.
            parent_output=cargo.CrateBuildOutput.BUILDRS,
            dep_name="child-name",
            parent_requested_features_of_dep=["feature1", "feature2"],
            dep_is_proc_macro=False,
            dep_for_first_party_code=for_first_party_code,
            dep_build_script_outputs=build_script_outputs)

        # The new crate was added to new_keys
        self.assertSetEqual(new_keys, {dep_key})

        # The child crate should be in the BuildData
        self.assertSetEqual(build_data.for_buildrs.all_crates(), {dep_key})
        self.assertSetEqual(build_data.for_normal.all_crates(), {dep_key})
        self.assertSetEqual(build_data.for_tests.all_crates(), {dep_key})
        # In buildrs usage, (since the parent was building BUILDRS output),
        # the dependency will be used and have data populated.
        with build_data.for_buildrs.per_crate(dep_key) as crate:
            # The requested features will be used on _all_ architectures, even
            # since the ArchSpecific tests one but applies to all.
            features = crate.features.all_archsets()
            self.assertListSortedEqual(features, [
                ("feature1", compiler.ArchSet.ALL()),
                ("feature2", compiler.ArchSet.ALL()),
            ])
            # Similarly, the crate is used everywhere. This should always be a
            # superset of the architectures where features are enabled.
            self.assertEqual(crate.used_on_archs, compiler.ArchSet.ALL())
            # Since the ArchSpecific said the parent is not arch-specific, and
            # there's no arch-specific deps, the dependency will not be arch-
            # specific.
            self.assertFalse(crate.arch_specific)
            self.assertEqual(crate.for_first_party, for_first_party_code)
            self.assertEqual(crate.build_script_outputs, build_script_outputs)
            # There's no edges from the new dependency since we haven't added
            # those.
            for o in cargo.CrateBuildOutput:
                self.assertSetEqual(crate.deps[o].all_deps(),
                                    set(),
                                    msg="For output type {}".format(o))

        # Other usages are not used, and have no features enabled, since the
        # parent crate is only using this dependency for tests so far, as those
        # are the only edges we added.
        with build_data.for_normal.per_crate(dep_key) as crate:
            self.assertListSortedEqual(crate.features.all_archsets(), [])
            self.assertEqual(crate.used_on_archs, compiler.ArchSet.EMPTY())
            self.assertFalse(crate.arch_specific)
            self.assertEqual(crate.for_first_party, for_first_party_code)
            self.assertEqual(crate.build_script_outputs, build_script_outputs)
        with build_data.for_tests.per_crate(dep_key) as crate:
            self.assertListSortedEqual(crate.features.all_archsets(), [])
            self.assertEqual(crate.used_on_archs, compiler.ArchSet.EMPTY())
            self.assertFalse(crate.arch_specific)
            self.assertEqual(crate.for_first_party, for_first_party_code)
            self.assertEqual(crate.build_script_outputs, build_script_outputs)

        # The parent would have edges to the child but this was a dep from the
        # third_party.toml, so there's no parent.

        # Now add a transitive dependency, which will have a parent: the child
        # that we added above. We will only add edges to the grand child on
        # Windows.
        parent_key = dep_key
        for_first_party_code = False
        build_script_outputs = {"src/grand-child-outs.rs"}
        dep_key = self.add_fake_dependency_edges(
            build_data,
            new_keys,
            parent_name="child-name",  # The parent crate of the grand-child.
            # Parent used everywhere.
            archset_where_parent_used=compiler.ArchSet.ALL(),
            parent_is_arch_specific=False,
            parent_has_arch_specific_deps=True,
            # Act as if cargo-tree sees the grand-child on Windows, so we're
            # adding an edge there. Since parent_has_arch_specific_deps is True,
            # the edge won't be added to other architectures automatically.
            computing_arch="x86_64-pc-windows-msvc",
            # Parent was used for buildrs.
            parent_usage=cargo.CrateUsage.FOR_BUILDRS,
            # Adding a build-dependency.
            parent_output=cargo.CrateBuildOutput.BUILDRS,
            dep_name="grand-child-name",
            parent_requested_features_of_dep=["featureA", "featureB"],
            dep_is_proc_macro=False,
            dep_for_first_party_code=False,
            dep_build_script_outputs=build_script_outputs)
        current_archset = compiler.ArchSet(initial={"x86_64-pc-windows-msvc"})

        # The new crate was added to new_keys
        self.assertSetEqual(new_keys, {parent_key, dep_key})

        # The grand-child crate should be in the BuildData
        self.assertSetEqual(build_data.for_buildrs.all_crates(),
                            {parent_key, dep_key})
        self.assertSetEqual(build_data.for_normal.all_crates(),
                            {parent_key, dep_key})
        self.assertSetEqual(build_data.for_tests.all_crates(),
                            {parent_key, dep_key})
        # In buildrs usage, (since its parent was building BUILDRS output),
        # the dependency will be used and have data populated.
        with build_data.for_buildrs.per_crate(dep_key) as crate:
            # The requested features will be used on the architecture we're
            # processing, since they can't be generalized to all architectures.
            features = crate.features.all_archsets()
            self.assertListSortedEqual(features,
                                       [("featureA", current_archset),
                                        ("featureB", current_archset)])
            # Similarly, the crate is used on the architecture we're processing.
            self.assertEqual(crate.used_on_archs, current_archset)
            # Since the dep is in a target rule (parent_has_arch_specific_deps),
            # the crate is considered arch-specific.
            self.assertTrue(crate.arch_specific)
            self.assertEqual(crate.for_first_party, for_first_party_code)
            self.assertEqual(crate.build_script_outputs, build_script_outputs)
            # There's no edges from the new dependency since we haven't added
            # those.
            for o in cargo.CrateBuildOutput:
                self.assertSetEqual(crate.deps[o].all_deps(),
                                    set(),
                                    msg="For output type {}".format(o))

        # Other usages are not used, and have no features enabled, since the
        # parent crate is only using this dependency for tests so far, as those
        # are the only edges we added.
        with build_data.for_normal.per_crate(dep_key) as crate:
            self.assertListSortedEqual(crate.features.all_archsets(), [])
            self.assertEqual(crate.used_on_archs, compiler.ArchSet.EMPTY())
            self.assertFalse(crate.arch_specific)
            self.assertEqual(crate.for_first_party, for_first_party_code)
            self.assertEqual(crate.build_script_outputs, build_script_outputs)
        with build_data.for_tests.per_crate(dep_key) as crate:
            self.assertListSortedEqual(crate.features.all_archsets(), [])
            self.assertEqual(crate.used_on_archs, compiler.ArchSet.EMPTY())
            self.assertFalse(crate.arch_specific)
            self.assertEqual(crate.for_first_party, for_first_party_code)
            self.assertEqual(crate.build_script_outputs, build_script_outputs)

        # The child crate now depends on the grand-child crate on the computed.
        # architecture, only for it's buildrs script.
        with build_data.for_buildrs.per_crate(parent_key) as crate:
            self.assertSetEqual(
                crate.deps[cargo.CrateBuildOutput.BUILDRS].all_deps(),
                {dep_key})
            self.assertSetEqual(
                crate.deps[cargo.CrateBuildOutput.NORMAL].all_deps(), set())
            self.assertSetEqual(
                crate.deps[cargo.CrateBuildOutput.TESTS].all_deps(), set())
        # We haven't considered the parent crate being built into a crate's
        # lib/bin as a normal dependency, or tests as a dev-dependency so
        # there's no edges.
        with build_data.for_normal.per_crate(parent_key) as crate:
            for o in cargo.CrateBuildOutput:
                self.assertSetEqual(crate.deps[o].all_deps(),
                                    set(),
                                    msg="For output type {}".format(o))
        with build_data.for_tests.per_crate(parent_key) as crate:
            for o in cargo.CrateBuildOutput:
                self.assertSetEqual(crate.deps[o].all_deps(),
                                    set(),
                                    msg="For output type {}".format(o))

        new_keys = set()
        # Now we again compute edges between the child and grand-child, for
        # another architecture, and for the child's normal build output. It
        # can use different features of the grand-child there.
        dep_key = self.add_fake_dependency_edges(
            build_data,
            new_keys,
            parent_name="child-name",  # The parent crate of the grand-child.
            # Parent used everywhere.
            archset_where_parent_used=compiler.ArchSet.ALL(),
            parent_is_arch_specific=False,
            parent_has_arch_specific_deps=True,
            # Act as if cargo-tree sees the grand-child on Fuchsia, so we're
            # adding an edge there. Since parent_has_arch_specific_deps is True,
            # the edge won't be added to other architectures automatically.
            computing_arch="x86_64-fuchsia",
            # Parent was used for a lib/bin.
            parent_usage=cargo.CrateUsage.FOR_NORMAL,
            # Adding a build-dependency.
            parent_output=cargo.CrateBuildOutput.BUILDRS,
            dep_name="grand-child-name",
            parent_requested_features_of_dep=["featureB", "featureC"],
            dep_is_proc_macro=False,
            dep_for_first_party_code=False,
            dep_build_script_outputs=build_script_outputs)
        current_archset = compiler.ArchSet(initial={"x86_64-fuchsia"})
        before_archset = compiler.ArchSet(initial={"x86_64-pc-windows-msvc"})
        union_archset = compiler.ArchSet(
            initial={"x86_64-pc-windows-msvc", "x86_64-fuchsia"})

        # The grand-child is being used on a new platform, and is arch-specific,
        # so it will need to compute its deps again.
        self.assertSetEqual(new_keys, {dep_key})

        # In buildrs usage, (since its parent was building BUILDRS output),
        # the dependency will be used and have data populated.
        with build_data.for_buildrs.per_crate(dep_key) as crate:
            # The requested features will be used on the architecture we're
            # processing, since they can't be generalized to all architectures.
            features = crate.features.all_archsets()
            self.assertListSortedEqual(features, [
                ("featureA", before_archset),
                ("featureB", union_archset),
                ("featureC", current_archset),
            ])
            # The crate is used on the union architectures.
            self.assertEqual(crate.used_on_archs, union_archset)
            # There's no edges from the new dependency since we haven't added
            # those.
            for o in cargo.CrateBuildOutput:
                self.assertSetEqual(crate.deps[o].all_deps(),
                                    set(),
                                    msg="For output type {}".format(o))

        # Other usages are not used, and have no features enabled, since the
        # parent crate is only using this dependency for tests so far, as those
        # are the only edges we added.
        with build_data.for_normal.per_crate(dep_key) as crate:
            self.assertListSortedEqual(crate.features.all_archsets(), [])
            self.assertEqual(crate.used_on_archs, compiler.ArchSet.EMPTY())
            self.assertFalse(crate.arch_specific)
            self.assertEqual(crate.for_first_party, for_first_party_code)
            self.assertEqual(crate.build_script_outputs, build_script_outputs)
        with build_data.for_tests.per_crate(dep_key) as crate:
            self.assertListSortedEqual(crate.features.all_archsets(), [])
            self.assertEqual(crate.used_on_archs, compiler.ArchSet.EMPTY())
            self.assertFalse(crate.arch_specific)
            self.assertEqual(crate.for_first_party, for_first_party_code)
            self.assertEqual(crate.build_script_outputs, build_script_outputs)

        # The child crate still depends on the grand-child crate for its buildrs
        # script when building built as a build-dependency.
        with build_data.for_buildrs.per_crate(parent_key) as crate:
            self.assertSetEqual(
                crate.deps[cargo.CrateBuildOutput.BUILDRS].all_deps(),
                {dep_key})
            self.assertSetEqual(
                crate.deps[cargo.CrateBuildOutput.NORMAL].all_deps(), set())
            self.assertSetEqual(
                crate.deps[cargo.CrateBuildOutput.TESTS].all_deps(), set())
        # The child crate now _also_ depends on the grand-child crate for its
        # buildrs script when building built as a normal dependency.
        with build_data.for_normal.per_crate(parent_key) as crate:
            self.assertSetEqual(
                crate.deps[cargo.CrateBuildOutput.BUILDRS].all_deps(),
                {dep_key})
            self.assertSetEqual(
                crate.deps[cargo.CrateBuildOutput.NORMAL].all_deps(), set())
            self.assertSetEqual(
                crate.deps[cargo.CrateBuildOutput.TESTS].all_deps(), set())
        # We haven't considered the parent crate being built into a crate's
        # tests as a dev-dependency so there's no edges.
        with build_data.for_tests.per_crate(parent_key) as crate:
            for o in cargo.CrateBuildOutput:
                self.assertSetEqual(crate.deps[o].all_deps(),
                                    set(),
                                    msg="For output type {}".format(o))
